﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace TA.JSON
{
	/// <summary>
	/// Parses raw JSON into tokens and values.
	/// </summary>
	public class JsonReader : IJsonReader
	{
		#region PUBLIC
		#region DECLARATIONS
		#endregion
		#region INIT
		/// <summary>
		/// Ctor.
		/// </summary>
		/// <param name="tr">Source.</param>
		public JsonReader(TextReader tr)
		{
			this._tr = tr;
		}
		#endregion
		#region PROPS
		/// <summary>
		/// Current state of the reader.
		/// </summary>
		public JsonState CurrentState
		{
			get
			{
				return this._currentState;
			}
		}
		/// <summary>
		/// The type of the current token.
		/// </summary>
		public JsonToken TokenType
		{
			get
			{
				if (!this._tokenReady)
					this.ReadToken();
				return this._tokenType;
			}
		}
		/// <summary>
		/// The value of the current token. If token is property name, this will return the name.
		/// </summary>
		public object Value
		{
			get
			{
				if (!this._tokenReady)
					this.ReadToken();
				return this._value;
			}
		}
		/// <summary>
		/// The current depth of nested objects and arrays.
		/// </summary>
		public int Depth
		{
			get
			{
				return this._stack.Count;
			}
		}
		#endregion
		#region METHODS
		/// <summary>
		/// Skips the current token and moves to the next.
		/// </summary>
		public void Read()
		{
			this._tokenReady = false;
		}
		#endregion
		#endregion
		#region INTERNAL
		#region VARS
		TextReader _tr;
		Stack<JsonState> _stack = new Stack<JsonState>();
		JsonState _currentState;
		JsonToken _tokenType;
		bool _tokenReady = false;
		object _value = null;
		bool _first = false;
		#endregion
		#region INIT
		#endregion
		#region PROPS
		#endregion
		#region METHODS
		void PushState(JsonState s)
		{
			this._currentState = s;
			this._stack.Push(s);
			this._first = true;
		}
		void PopState()
		{
			this._stack.Pop();
			this._first = false;
			if (this._stack.Count == 0)
				this._currentState = JsonState.None;
			else
				this._currentState = this._stack.Peek();
		}
		void VerifyState(JsonState[] states)
		{
			for (var i = 0; i < states.Length; ++i)
			{
				if (states[i] == this._currentState)
					return;
			}

			StringBuilder sbStates = new StringBuilder("Expected states ");
			foreach (JsonState s in states)
			{
				sbStates.Append(s.ToString());
				sbStates.Append(", ");
			}
			sbStates.Append("but encountered ");
			sbStates.Append(this._currentState);
			throw new InvalidOperationException(sbStates.ToString());
		}
		void VerifyStateValue()
		{
			this.VerifyState(new[] { JsonState.None, JsonState.ObjectValue, JsonState.Array });
		}
		private void ReadToken()
		{
			this._value = null;

			bool flag;
			bool commaDone = false, minusDone = false;
			do
			{
				flag = false;
				char c = (char)this._tr.Peek();
				switch (c)
				{
					case ' ':
					case '\t':
					case '\r':
					case '\n':
						flag = true;
						this._tr.Read();
						break;
					case '{':
						this.FoundValue();
						this.PushState(JsonState.Object);
						this._tokenType = JsonToken.StartObject;
						this._tr.Read();
						break;
					case '}':
						this.VerifyState(new[] { JsonState.Object });
						this.PopState();
						this._tokenType = JsonToken.EndObject;
						this._tr.Read();
						break;
					case '[':
						this.FoundValue();
						this.PushState(JsonState.Array);
						this._tokenType = JsonToken.StartArray;
						this._tr.Read();
						break;
					case ']':
						this.VerifyState(new[] { JsonState.Array });
						this.PopState();
						this._tokenType = JsonToken.EndArray;
						this._tr.Read();
						break;
					case ',':
						if (this._first)
							throw new JsonException("found comma before any items in the list.");
						if (commaDone)
							throw new JsonException("too many commas.");

						flag = true;
						commaDone = true;
						this._tr.Read();
						break;
					case '"':
						this._tr.Read();

						StringBuilder sbString = new StringBuilder();
						char c2 = (char)this._tr.Read();
						while (c2 != '"')
						{
							if (c2 == '\\')
							{
								char c3 = (char)this._tr.Read();
								switch (c3)
								{
									case 'r': sbString.Append('\r'); break;
									case 'n': sbString.Append('\n'); break;
									case 'b': sbString.Append('\b'); break;
									case 't': sbString.Append('\t'); break;
									case 'f': sbString.Append('\f'); break;
									case '\\': sbString.Append('\\'); break;
									case '\"': sbString.Append('"'); break;
									default:
										throw new JsonException("bad character '" + c3 + "' after '\\'.");
								}
							}
							else
							{
								sbString.Append(c2);
							}

							c2 = (char)this._tr.Read();
						}

						this._value = sbString.ToString();
						if (this._currentState == JsonState.Object)
						{ // property name
							Expect(':');

							this._tokenType = JsonToken.PropertyName;
							this.PushState(JsonState.ObjectValue);
						}
						else
						{ // then string
							this._tokenType = JsonToken.String;
							this.FoundValue();
						}
						break;
					case '-': // minus before a number
						if (minusDone)
							throw new JsonException("too many minus signs.");

						this._tr.Read();
						flag = true;
						minusDone = true;
						break;
					default:
						if (char.IsLetter(c) || c == '_') // some word
						{
							string word = ReadIdentifier();
							if (word.Equals("true") || word.Equals("false"))
							{
								this.FoundValue();
								this._value = bool.Parse(word);
								this._tokenType = JsonToken.Boolean;
							}
							else if (word.Equals("null"))
							{
								this.FoundValue();
								this._tokenType = JsonToken.Null;
								this._value = null;
							}
							else if (word.Equals("new")) // maybe "new Date(...)"
							{
								string word2 = ReadIdentifier();
								if (!word2.Equals("Date"))
								{
									throw new JsonException("Expected \"Date\" after \"new\" but found \"" + word2 + "\"");
								}
								else
								{
									int year, month, day, hours, minutes, seconds, milliseconds;
									Expect('(');
									year = (int)ReadNumber();
									Expect(',');
									month = (int)ReadNumber();
									Expect(',');
									day = (int)ReadNumber();
									Expect(',');
									hours = (int)ReadNumber();
									Expect(',');
									minutes = (int)ReadNumber();
									Expect(',');
									seconds = (int)ReadNumber();
									Expect(',');
									milliseconds = (int)ReadNumber();
									Expect(')');

									this.VerifyStateValue();

									this._tokenType = JsonToken.DateTime;
									this._value = new DateTime(year, month, day, hours, minutes, seconds, milliseconds);

									this.FoundValue();
								}
							}
							else
							{ // then must be a property name
								Expect(':');
								this.VerifyState(new[] { JsonState.Object });
								this.PushState(JsonState.ObjectValue);
								this._tokenType = JsonToken.PropertyName;
								this._value = word;
							}
							//if(this._currentState != JsonState.Object)
						}
						else if (char.IsDigit(c))
						{
							long number = this.ReadNumber();
							if (minusDone) number = -number;

							this.FoundValue();
							if ((char)this._tr.Peek() == '.')
							{
								this._tr.Read();
								long decimals = this.ReadNumber();
								int numDecimals = (int)Math.Ceiling(Math.Log10((double)decimals));
								this._value = (double)number + (double)decimals / Math.Pow(10, numDecimals);
								this._tokenType = JsonToken.Float;
							}
							else
							{
								this._value = number;
								this._tokenType = JsonToken.Integer;
							}
						}
						break;
				}
			}
			while (flag);
			this._tokenReady = true;
		}

		private string ReadIdentifier()
		{
			SkipWhiteSpaces();

			StringBuilder sbWord = new StringBuilder();
			while (char.IsLetterOrDigit((char)this._tr.Peek()) || (char)this._tr.Peek() == '_')
			{
				sbWord.Append((char)this._tr.Read());
			}
			return sbWord.ToString();
		}

		private void FoundValue()
		{
			this.VerifyStateValue();
			if (this._currentState == JsonState.ObjectValue)
				this.PopState();
			this._first = false;
		}

		private long ReadNumber()
		{
			this.SkipWhiteSpaces();

			long res = 0;
			bool good = false;
			while (char.IsDigit((char)this._tr.Peek()))
			{
				good = true;
				res = res * 10 + this._tr.Read() - '0';
			}
			if (!good)
				throw new JsonException("expected a digit but found '" + (char)this._tr.Peek() + "'");
			return res;
		}

		private void Expect(char p)
		{
			this.SkipWhiteSpaces();
			char c = (char)this._tr.Read();
			if (c != p)
				throw new JsonException("expected '" + p + "' but found '" + c + "'");
		}

		private void SkipWhiteSpaces()
		{
			while (char.IsWhiteSpace((char)this._tr.Peek()))
			{
				this._tr.Read();
			}
		}
		#endregion
		#endregion

	}
}
